//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//--------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Text;
using UnityEngine;

using Flare.Display;
using Flare.Geom;

namespace Flare.Text
{
    /// <summary>
    /// StaticText class represents static text objects on the display list. Instances of this
    /// class are created using authoring tools and not via code.
    /// </summary>
    public class StaticText : DisplayObject
    {
        /// <summary>
        /// Gets the text contained in the static text object.
        /// </summary>
        public string text { get; 
            /* \cond */ private set; /* \endcond */ }

        internal Rectangle textBounds { get; private set; }
        internal Matrix textMatrix { get; private set; }
        internal TextRecord[] textRecords { get; private set; }

        private Mesh mesh = null;
        private Dictionary<TextRecord, uint> meshFontTextureRevisions = null;
        private Dictionary<TextRecord, uint> submeshIndex = null;
        private float meshScale = float.NaN;

        internal StaticText(Rectangle textBounds, Matrix textMatrix, TextRecord[] textRecords)
        {
            this.textBounds = textBounds;
            this.textMatrix = textMatrix;
            this.textRecords = textRecords;

            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < this.textRecords.Length; ++i)
            {
                builder.Append(this.textRecords[i].text);
                if (i < (this.textRecords.Length - 1))
                {
                    builder.Append('\n');
                }
            }
            this.text = builder.ToString();
        }

        internal override bool HitTest(float x, float y, bool shapeFlag, bool evaluateChildren)
        {
            Point localPoint = GlobalToLocal(new Point(x, y));

            if (shapeFlag)
            {
                if (this.textBounds.ContainsPoint(localPoint))
                {
                    // TODO bwmott 2013-08-17: Here we should look at the individual glyphes to
                    // decide if there's a hit or not.
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return this.textBounds.ContainsPoint(localPoint);
            }
        }

        public override object Clone()
        {
            // Return a clone of the Shape with a deep copy of any mutable members
            StaticText clone = (StaticText)base.Clone();

            // Clear out the mesh cache so it'll be regenerated for this copy
            clone.mesh = null;
            clone.meshFontTextureRevisions = null;
            clone.submeshIndex = null;
            clone.meshScale = float.NaN;

            return clone;
        }

        private static Material ms_textMaterial = null;
        private static Matrix ms_tmpMatrix = new Matrix();

        internal override void Render(RenderContext context)
        {
            if (!this.visible || this.isMask)
            {
                return;
            }

            if (this.mask != null)
            {
                context.PushMask(this.mask);
            }
            
            context.SetupMasks();

            // Get the text rendering material if it isn't already available
            if (ms_textMaterial == null)
            {
                Shader shader = Shader.Find("Flare/Text");
                if (shader == null)
                {
                    Log.Error(Subsystem.Playback, "Could not find 'Flare/Text' shader. " +
                        "Please ensure it is in a Resource folder.");
                }
                else
                {
                    ms_textMaterial = new Material(shader);
                }
            }

            // Create mesh for the records using device fonts if the mesh doesn't already exist
            if (this.mesh == null)
            {
                CreateDeviceFontMesh();
            }

            float scale = (float)(context.stageTranslationAndScale.d * global::System.Math.Sqrt(
                this.transform.concatenatedMatrix.c * this.transform.concatenatedMatrix.c +
                this.transform.concatenatedMatrix.d * this.transform.concatenatedMatrix.d));

            ColorTransform cxform = this.transform.concatenatedColorTransform;
            ms_tmpMatrix.CopyFrom(this.textMatrix);
            ms_tmpMatrix.Concat(this.transform.concatenatedMatrix);

            GL.PushMatrix();
            GL.LoadIdentity();

            int submeshIndex = 0;
            for (int i = 0; i < this.textRecords.Length; ++i)
            {
                var textRecord = this.textRecords[i];
                DeviceFont deviceFont = textRecord.deviceFont;

                // If there's a device font then render the corresponding submesh. In general,
                // this is the path that most rendering should take since it has the best
                // loading and rendering times.
                if (deviceFont != null)
                {
                    // Ensure the mesh for the given text record is up-to-date
                    UpdateDeviceFontMesh(textRecord, scale);

                    ms_textMaterial.color = cxform.ApplyToARGB(textRecord.color);
                    ms_textMaterial.mainTexture = deviceFont.unityFont.material.mainTexture;
                    ms_textMaterial.SetPass(0);

                    Matrix4x4 mat = Matrix4x4.TRS(
                        new Vector3(textRecord.xOffset, textRecord.yOffset, 0),
                        Quaternion.identity, new Vector3(1.0f / scale, 1.0f / scale, 0));
                    mat = (Matrix4x4)(this.textMatrix) * mat;
                    mat = (Matrix4x4)(this.transform.concatenatedMatrix) * mat;
                    UnityEngine.Graphics.DrawMeshNow(mesh, mat, submeshIndex);

                    submeshIndex += 1;
                }
                // Device font not available so try embedded font textures or shape rendering
                else
                {
                    float xoff = textRecord.xOffset;
                    float yoff = textRecord.yOffset;

                    if (Application.HasProLicense())
                    {
                        // Rendered textures are available so let's use them
                        foreach (var glyphEntry in textRecord.glyphEntries)
                        {
                            ms_textMaterial.color = cxform.ApplyToARGB(textRecord.color);
                            ms_textMaterial.mainTexture =
                                textRecord.embeddedFont.GetGlyphTexture(glyphEntry.index);
                            ms_textMaterial.SetPass(0);
    
                            Matrix m = new Matrix(textRecord.textHeight / 1024.0f, 0.0f, 0.0f,
                                textRecord.textHeight / 1024.0f, xoff,
                                yoff + (textRecord.textHeight * EmbeddedFont.baseline) / 1024.0f);
                            m.Concat(ms_tmpMatrix);
    
                            GL.PushMatrix();
                            GL.MultMatrix((Matrix4x4)m);
                            GL.Begin(GL.QUADS);
                            GL.TexCoord2(0, 0);
                            GL.Vertex3(0, 0, 0);
                            GL.TexCoord2(0, 1);
                            GL.Vertex3(0, -1024, 0);
                            GL.TexCoord2(1, 1);
                            GL.Vertex3(1024, -1024, 0);
                            GL.TexCoord2(1, 0);
                            GL.Vertex3(1024, 0, 0);
                            GL.End();
                            GL.PopMatrix();
    
                            xoff += glyphEntry.advance;
                        }
                    }
                    else
                    {
                        // Compute final color transform for text record. Zero out the multiplier
                        // fields so the color associated with the glyph shape is ignored.
                        ColorTransform ct = new ColorTransform(cxform);
                        ct.Concat(new ColorTransform(1.0f, 1.0f, 1.0f, 1.0f,
                            ((textRecord.color & 0x00ff0000) >> 16),
                            ((textRecord.color & 0x0000ff00) >> 8),
                            ((textRecord.color & 0x000000ff)),
                            ((textRecord.color & 0xff000000) >> 24)));
                        ct.redMultiplier = 0.0f;
                        ct.greenMultiplier = 0.0f;
                        ct.blueMultiplier = 0.0f;
                        ct.alphaMultiplier = 0.0f;

                        foreach (var glyphEntry in textRecord.glyphEntries)
                        {
                            Shape shape = textRecord.embeddedFont.glyphShapes[glyphEntry.index];
                            Matrix m = new Matrix(textRecord.textHeight / 1024.0f, 0.0f, 0.0f,
                                textRecord.textHeight / 1024.0f, xoff, yoff);
                            m.Concat(ms_tmpMatrix);
                            shape.graphics.Render(m, ct);
                            xoff += glyphEntry.advance;
                        }
                    }
                }
            }

            this.meshScale = scale;

            GL.PopMatrix();

            if (this.mask != null)
            {
                context.PopMask();
            }
        }

        internal override void RenderAsMask(RenderContext context)
        {
            // TODO bwmott 2014-06-10: Need to render text using mask shader
        }

        private void CreateDeviceFontMesh()
        {
            this.mesh = new Mesh();
            this.meshFontTextureRevisions = new Dictionary<TextRecord, uint>();
            this.submeshIndex = new Dictionary<TextRecord, uint>();
            this.meshScale = float.NaN;
            
            List<Vector3> vertices = new List<Vector3>();
            List<Vector2> uvs = new List<Vector2>();
            List<int[]> indices = new List<int[]>();
            
            // Create a submesh for each of the text records having a device font
            int index = 0;
            foreach (var textRecord in this.textRecords)
            {
                // Skip record which do not have a device font
                if (textRecord.deviceFont == null)
                {
                    continue;
                }

                this.submeshIndex[textRecord] = (uint)index;
                List<int> submeshIndices = new List<int>();
                for (int i = 0; i < textRecord.glyphEntries.Length; ++i)
                {
                    vertices.Add(Vector3.zero);
                    vertices.Add(Vector3.zero);
                    vertices.Add(Vector3.zero);
                    vertices.Add(Vector3.zero);
                    
                    uvs.Add(Vector2.zero);
                    uvs.Add(Vector2.zero);
                    uvs.Add(Vector2.zero);
                    uvs.Add(Vector2.zero);
                    
                    int count = vertices.Count;
                    submeshIndices.Add(count - 4);
                    submeshIndices.Add(count - 3);
                    submeshIndices.Add(count - 2);
                    submeshIndices.Add(count - 4);
                    submeshIndices.Add(count - 2);
                    submeshIndices.Add(count - 1);

                    index += 4;
                }
                indices.Add(submeshIndices.ToArray());
            }
            
            if (indices.Count > 0)
            {
                this.mesh.MarkDynamic();
                this.mesh.subMeshCount = indices.Count;
                this.mesh.vertices = vertices.ToArray();
                this.mesh.uv = uvs.ToArray();
                for (int i = 0; i < indices.Count; ++i)
                {
                    this.mesh.SetTriangles(indices[i], i);
                }
            }
        }

        private void UpdateDeviceFontMesh(TextRecord textRecord, float scale)
        {
            DeviceFont font = textRecord.deviceFont;

            if ((this.meshScale != scale) || 
                (this.meshFontTextureRevisions[textRecord] != font.unityFontTextureRevision))
            {
                Vector3[] vertices = this.mesh.vertices;
                Vector2[] uv = this.mesh.uv;

                int size = (int)(textRecord.textHeight * scale);
                
                // Ensure characters are in font texture and remember its revision
                textRecord.deviceFont.unityFont.RequestCharactersInTexture(textRecord.text, size);
                meshFontTextureRevisions[textRecord] =
                    textRecord.deviceFont.unityFontTextureRevision;
                
                float xoff = 0.0f;
                float yoff = textRecord.deviceFont.vertexOffset;

                int index = (int)this.submeshIndex[textRecord];
                for (int j = 0; j < textRecord.glyphEntries.Length; ++j)
                {
                    var glyphEntry = textRecord.glyphEntries[j];

                    char c = textRecord.embeddedFont.codeTable[glyphEntry.index];
                    CharacterInfo ci;
                    if (textRecord.deviceFont.unityFont.GetCharacterInfo(c, out ci, size))
                    {
#if UNITY_5
                        Vector2 v0 = new Vector2(xoff + ci.minX, yoff - ci.maxY);
                        Vector2 v1 = new Vector2(xoff + ci.maxX, yoff - ci.minY);
                        
                        vertices[index + 0] = new Vector3(v0.x, v0.y, 0.0f);
                        vertices[index + 1] = new Vector3(v1.x, v0.y, 0.0f);
                        vertices[index + 2] = new Vector3(v1.x, v1.y, 0.0f);
                        vertices[index + 3] = new Vector3(v0.x, v1.y, 0.0f);

                        uv[index + 0] = ci.uvTopLeft;
                        uv[index + 1] = ci.uvTopRight;
                        uv[index + 2] = ci.uvBottomRight;
                        uv[index + 3] = ci.uvBottomLeft; 
#else
                        Vector2 v0 = new Vector2(xoff + ci.vert.xMin, yoff - ci.vert.yMax);
                        Vector2 v1 = new Vector2(xoff + ci.vert.xMax, yoff - ci.vert.yMin);
                        Vector2 u0 = new Vector2(ci.uv.xMin, ci.uv.yMin);
                        Vector2 u1 = new Vector2(ci.uv.xMax, ci.uv.yMax);

                        vertices[index + 0] = new Vector3(v1.x, v0.y, 0.0f);
                        vertices[index + 1] = new Vector3(v0.x, v0.y, 0.0f);
                        vertices[index + 2] = new Vector3(v0.x, v1.y, 0.0f);
                        vertices[index + 3] = new Vector3(v1.x, v1.y, 0.0f);

                        bool flip = ci.flipped;
                        uv[index + 0] = flip ? new Vector2(u0.x, u1.y) : new Vector2(u1.x, u0.y);
                        uv[index + 1] = new Vector2(u0.x, u0.y);
                        uv[index + 2] = flip ? new Vector2(u1.x, u0.y) : new Vector2(u0.x, u1.y);
                        uv[index + 3] = new Vector2(u1.x, u1.y); 
#endif
                        xoff += glyphEntry.advance * scale;
                        index += 4;
                    }
                }

                this.mesh.vertices = vertices;
                this.mesh.uv = uv;
                this.mesh.RecalculateBounds();
            }
        }

        //--------------------------------------------------------------------------------

        internal class TextRecord
        {
            public EmbeddedFont embeddedFont { get; private set; }
            public DeviceFont deviceFont { get; private set; }
            public uint color { get; private set; }
            public float xOffset { get; private set; }
            public float yOffset { get; private set; }
            public float textHeight { get; private set; }
            public GlyphEntry[] glyphEntries { get; private set; }
            public string text { get; private set; }

            public TextRecord(EmbeddedFont font, uint color, float xOffset, float yOffset,
                float textHeight, GlyphEntry[] glyphEntries)
            {
                this.embeddedFont = font;
                this.color = color;
                this.xOffset = xOffset;
                this.yOffset = yOffset;
                this.textHeight = textHeight;
                this.glyphEntries = glyphEntries;

                // Lookup device font with the same name as the embedded font (if it exists)
                this.deviceFont = Font.GetDeviceFont(this.embeddedFont.fontName,
                    this.embeddedFont.fontStyle, false); 

                // Build text representation of glyph entries
                StringBuilder builder = new StringBuilder();
                foreach (var glyphEntry in this.glyphEntries)
                {
                    builder.Append(this.embeddedFont.codeTable[glyphEntry.index]);
                }
                this.text = builder.ToString();
            }
        }

        //--------------------------------------------------------------------------------

        internal class GlyphEntry
        {
            public uint index { get; private set; }
            public float advance { get; private set; }

            public GlyphEntry(uint index, float advance)
            {
                this.index = index;
                this.advance = advance;
            }
        }
    }
}